﻿using System;
using System.Collections.Generic;
using System.Linq;
using LunchCommand.Data;
using LunchCommand.Models;
using LunchCommand.Models.Logging;
using LunchCommand.Services.Maps;
using LunchCommand.ViewModels;

namespace LunchCommand.Services
{
    /// <summary>
    /// All of the lunchy intelligence of this application
    /// </summary>
    public class LunchService
    {
        private const bool QUICK = true;
        private const bool NOT_QUICK = false;
        private readonly bool? QUICK_OR_NOT = null;

        /// <summary>
        /// Absolutely maekss sure there can't be more than one lunch created at one time
        /// </summary>
        private static object lunchCreationLock = new object();

        /// <summary>
        /// Absolutely makes sure there can't be more than one lunch decision process going on at one time
        /// </summary>
        private static object lunchDecisionLock = new object();

        /// <summary>
        /// The number of days after which venues and genres are considered equally old
        /// </summary>
        private readonly Int32 maxDays;

        /// <summary>
        /// Constructor of the service
        /// </summary>        
        /// <param name="settings">The settings</param>
        /// <param name="logger">The logger</param>
        public LunchService(LunchCommandContext context, WebConfigSettings settings, ILogger logger)
        {
            this.DatabaseContext = context;
            this.LunchRepository = new LunchRepository(context);
            this.Settings = settings;
            this.Logger = logger;
            this.MapService = new BingMapsService(settings.BingMapsKey, logger);
            this.maxDays = settings.NumberOfDaysBeforeVenuesAreConsideredEquiallyOld;
        }

        /// <summary>
        /// The repo
        /// </summary>
        private LunchRepository LunchRepository { get; set; }

        /// <summary>
        /// The DB
        /// </summary>
        private LunchCommandContext DatabaseContext { get; set; }

        /// <summary>
        /// The service to get the travel time to the selected venue
        /// </summary>
        private IMapService MapService { get; set; }

        /// <summary>
        /// The logger to log with
        /// </summary>
        private ILogger Logger { get; set; }

        /// <summary>
        /// The settings
        /// </summary>
        private WebConfigSettings Settings { get; set; }

        /// <summary>
        /// Generates a LunchCommandViewModel for the provided user
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        public LunchCommandViewModel GenerateViewModel(User user)
        {
            var lunch = this.GetOrCreateLunch();

            return this.GenerateViewModel(user, lunch);
        }

        /// <summary>
        /// Whne the view model comes back in a POST, it only has whan was in the form fields.
        /// This method processes the fields the user changed and then recreated the view model
        /// for the page to refresh with
        /// </summary>
        /// <param name="vm">The view model that came back from the client</param>
        public void RehydrateAndProcessViewModel(ref LunchCommandViewModel vm)
        {
            var user = this.DatabaseContext.Users.Find(vm.User.UserId);
            var lunch = this.GetOrCreateLunch();

            var decision = user.LunchesDecided.FirstOrDefault(l => l.Lunch.LunchId == lunch.LunchId);
            var going = (bool?)null;

            // If the user actively chose to go today
            if (vm.UserHasDecidedToday && vm.UserIsGoingToday)
            {
                going = true;
            }
            else if (vm.UserHasDecidedToday /* and they are not going */)
            {
                going = false;
            }

            if (going.HasValue)
            {
                if (decision == null)
                {
                    user.LunchesDecided.Add(new LunchUser()
                    {
                        Decision = going.Value,
                        Lunch = lunch,
                        User = user
                    });
                }
                else
                {
                    decision.Decision = going.Value;
                }
            }

            // Venue Choices
            if (vm.VenueChoicesMade)
            {
                user.VenueDecisions.Clear();
                user.LastTimeVenueDecisionMade = DateTime.Now;

                // Yeses
                foreach (var v in vm.VenueChoicesForUser)
                {
                    user.VenueDecisions.Add(
                        new UserVenueChoice()
                        {
                            VenueId = v.VenueId,
                            Degree = v.Preference,
                            UserId = user.UserId
                        });
                }
            }

            this.DatabaseContext.Entry(user).State = System.Data.Entity.EntityState.Modified;

            this.DatabaseContext.SaveChanges();

            // reload lunch
            lunch = this.DatabaseContext.Lunches.Find(lunch.LunchId);

            vm = this.GenerateViewModel(user, lunch);
        }

        /// <summary>
        /// Gets the status of today's lunch.  If it's lunchtime and the venue hasn't been picked yet,
        /// this method picks the venue and saves that decision to the db.
        /// </summary>
        /// <returns>The status of today's lunch</returns>
        public LunchStatusViewModel GetStatus()
        {
            var status = new LunchStatusViewModel();
            lock (lunchDecisionLock)
            {
                var lunch = this.GetOrCreateLunch();

                // If it's not lunchtime...
                if (lunch.LunchTime > DateTime.Now)
                {
                    return new LunchStatusViewModel()
                    {
                        TimeLeft = lunch.LunchTime - DateTime.Now,
                        Status = LunchStatus.NotStarted
                    };
                }
                else
                {
                    // It's lunctime!
                    // If a venue hasn't been picked yet and one can possibly even be picked
                    if (lunch.Venue == null && lunch.UsersGoing.Count() > 0)
                    {
                        this.Logger.Log(LogEntryType.Info, "Lunch status requested but lunch needs to be decided first");

                        var allVenues = this.DatabaseContext.ActiveVenues.ToArray();

                        // Try to get a consensus for the main venue
                        var possibilties = this.GetPossibleVenues(allVenues, NOT_QUICK, lunch.UsersGoing);
                        if (!possibilties.Venues.Any())
                        {
                            this.Logger.Log(LogEntryType.Info, "Could not find any non-quick venues.  Resorting to quick ones for the main venue");
                            possibilties = this.GetPossibleVenues(allVenues, QUICK, lunch.UsersGoing);
                        }

                        var consensus = this.GetConsensus(possibilties.Venues, lunch, NOT_QUICK);

                        // If that fails, in all likelyhood, no one wanted to go to lunch today
                        if (consensus == null)
                        {
                            this.Logger.Log(LogEntryType.Info, "Lunch could not be determined today");
                            status.Status = LunchStatus.DidNotHappen;
                        }
                        else
                        {
                            lunch.Venue = consensus;
                            lunch.PercentOfUsersAgreeing = possibilties.PercentHappy;

                            // Handle the dissenting venue, if there needs to be one
                            if (possibilties.UnhappyUsers.Count() > 0)
                            {
                                this.Logger.Log(LogEntryType.Info, "Looking up a dissenting venue");

                                var dissentingPossibilities = this.GetPossibleVenues(allVenues, QUICK_OR_NOT, possibilties.UnhappyUsers);

                                // Get the possible venues for the users who don't like the chosen one
                                var dissentingConsesus = this.GetConsensus(dissentingPossibilities.Venues, lunch, NOT_QUICK);

                                if (dissentingConsesus != null)
                                {
                                    lunch.DissentingVenue = dissentingConsesus;
                                    lunch.PercentOfUsersDissenting = dissentingPossibilities.PercentHappy;
                                }
                            }

                            // Handle the quick venue.  If the decided venue is already quick, use that
                            if (lunch.Venue.IsQuick)
                            {
                                lunch.QuickVenue = lunch.Venue;
                                lunch.PercentOfUsersAgreeingToQuick = lunch.PercentOfUsersAgreeing;
                            }
                            else
                            {
                                this.Logger.Log(LogEntryType.Info, "Looking up a quick venue");

                                var quickPossibilties = this.GetPossibleVenues(allVenues, QUICK, lunch.UsersGoing);

                                consensus = this.GetConsensus(quickPossibilties.Venues, lunch, QUICK);

                                if (consensus != null)
                                {
                                    lunch.QuickVenue = consensus;
                                    lunch.PercentOfUsersAgreeingToQuick = possibilties.PercentHappy;
                                }
                            }
                        }

                        this.LunchRepository.Save(lunch);

                        this.DatabaseContext.Entry(lunch).Reload();
                    }
                } // End the lock, finally

                // Build the status
                status.DecidedVenue = lunch.Venue == null ? null : new VenueConsensusViewModel() { Venue = lunch.Venue, PercentageOfUsers = lunch.PercentOfUsersAgreeing.GetValueOrDefault(0) };
                status.DissentingVenue = lunch.DissentingVenue == null ? null : new VenueConsensusViewModel() { Venue = lunch.DissentingVenue, PercentageOfUsers = lunch.PercentOfUsersDissenting.GetValueOrDefault(0) };
                status.QuickVenue = lunch.QuickVenue == null ? null : new VenueConsensusViewModel() { Venue = lunch.QuickVenue, PercentageOfUsers = lunch.PercentOfUsersAgreeingToQuick.GetValueOrDefault(0) };

                status.Status = status.DecidedVenue == null ? LunchStatus.DidNotHappen : status.DissentingVenue == null ? LunchStatus.StartedWithConsensus : LunchStatus.StartedWithoutConsensus;

                try
                {
                    // Try to get the travel time to the decided venue
                    if (status.DecidedVenue != null && status.DecidedVenue.Venue != null)
                    {
                        var time = VenueTravelCache.GetTravelTime(lunch, status.DecidedVenue.Venue, this.Settings.StartingAddress, this.MapService);

                        if (time.TravelTime.HasValue)
                        {
                            status.MinutesToTravel = (int)time.TravelTime.Value.TotalMinutes;
                        }
                    }
                }
                catch { }

                return status;
            }
        }

        /// <summary>
        /// Generates a LunchCommandViewModel for the provided user and lunch
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        private LunchCommandViewModel GenerateViewModel(User user, Lunch lunch)
        {
            var allVenues = this.DatabaseContext.ActiveVenues.ToArray();
            var possibilities = this.GetPossibleVenues(allVenues, null, lunch.Users.Select(lu => lu.User));

            return new LunchCommandViewModel(user, lunch, allVenues, this.DatabaseContext.Users.ToArray(), possibilities.Venues);
        }

        /// <summary>
        /// Gets the venue to go to
        /// </summary>
        /// <param name="possibilities">The possible venues</param>
        /// <param name="forLunch">The lunch this is for</param>
        /// <returns>The venue decided upon</returns>
        private Venue GetConsensus(IEnumerable<Venue> possibilities, Lunch forLunch, Boolean forQuick)
        {
            if (!possibilities.Any())
            {
                this.Logger.Log(LogEntryType.Info, "There are no possibilities.  There can be no consensus");

                return null;
            }

            // narrow results down to the least-recently visited genres
            var genreDays = possibilities.Select(p => new { p = p, genreDays = Math.Min(p.Genre.GetDaysSinceLastVisited(forQuick, DateTime.MinValue), this.maxDays) });
            possibilities = genreDays.Where(p => p.genreDays == genreDays.Max(gd => gd.genreDays)).Select(p => p.p);

            this.Logger.Log(LogEntryType.Info, String.Format("Found {0} venues with genres visited least-recently", possibilities.Count()));

            if (possibilities.Count() <= 1)
            {
                return possibilities.FirstOrDefault();
            }
            else
            {
                // Further narrow down the choice to the least-recently visited venue
                // maxF is the oldest venue's age
                var maxf = possibilities
                    .Select(p2 => Math.Min(this.maxDays, p2.GetDaysSinceLastVisit(forQuick, DateTime.MinValue))).Max();

                possibilities = possibilities.Where(p =>
                    Math.Min(this.maxDays, p.GetDaysSinceLastVisit(forQuick, DateTime.MinValue)) == maxf);

                this.Logger.Log(LogEntryType.Info, String.Format("Found {0} venues visited least-recently", possibilities.Count()));

                if (possibilities.Count() <= 1)
                {
                    return possibilities.FirstOrDefault();
                }
                else
                {
                    var possibilitiesArray = possibilities.ToArray();
                    // Finally, just pick one at random
                    var index = new Random().Next(0, possibilitiesArray.Length);

                    this.Logger.Log(LogEntryType.Info, String.Format("Resorting to random venue.  Index: {0}", index));

                    return possibilitiesArray[index];
                }
            }
        }

        /// <summary>
        /// Gets all venues and the percentage of users in this lunch that want to go to them
        /// </summary>
        /// <param name="allVenues">The venues to choose from</param>
        /// <param name="quick">If true, only quick venues are considered.  If false, only not-quick or special venues are considered.  If null, all venues are considered</param>
        /// <param name="users">The users to consider</param>
        /// <returns></returns>
        internal VenueFilterResult GetPossibleVenues(IEnumerable<Venue> allVenues, Boolean? quick, IEnumerable<User> allUsers)
        {
            // Before we begin, remove any venue that will be impossible today.
            var filteredVenues = allVenues
                // Limit to quick/not-quick
                .Where(vc => vc.IsSpecial || !quick.HasValue || quick.Value == vc.IsQuick)
                // If we are looking at not-quick venues, remove any who have been visited too recently
                .Where(vc => (quick.HasValue && quick.Value) || (!vc.LastVisited.HasValue || ((DateTime.Now - vc.LastVisited.Value).TotalDays / 7.0) > vc.MinWeeksBetweenVisits))
                // If we are looking at quick venues, remove any who have been visited too recently
                .Where(vc => (!quick.HasValue || !quick.Value) || (!vc.LastVisitedAsQuick.HasValue || ((DateTime.Now - vc.LastVisitedAsQuick.Value).TotalDays / 7.0) > vc.MinWeeksBetweenVisits))
                // Remove any we can't visit today
                .Where(vc => vc.DaysOfWeek.CanGoOnDay(DateTime.Now.DayOfWeek))
                .Where(vc=>!vc.Deleted).Distinct().ToArray();

            var filteredVenueIds = new HashSet<Int32>(filteredVenues.Select(f => f.VenueID));

            // No use is doing anything else if there are no possible venues
            if (!filteredVenues.Any() || !allUsers.Any())
            {
                return new VenueFilterResult() { Venues = new Venue[0] };
            }

            // Here's what we will return
            var possibleVenues = new List<Venue>();

            // Order the users so the most-picky is on top so we can remove them later if we need to
            allUsers = allUsers.OrderByDescending(u => u.VenuesRefusing.Count());

            // We will update this as needed on each iteration
            var usersToRemove = 0;
            var result = new VenueFilterResult();

            do
            {
                var users = new LinkedList<User>(allUsers);
                var unhappyUsers = new LinkedList<User>();

                // Remove picky users.  This will happen if we couldn't get a consensus in previous iterations
                for (var i = 0; i < usersToRemove; i++)
                {
                    unhappyUsers.AddLast(users.First.Value);
                    users.RemoveFirst();
                }

                // Now we determine the actual venues 
                // First of all, add the venues that the users actually want to go to (as long as they are in the filtered list)
                foreach (var u in users)
                {
                    possibleVenues.AddRange(u.VenuesPreferring.Where(vc => filteredVenueIds.Contains(vc.VenueID)));
                }

                // Second, remove all of the venues that the users don't want to go to
                var impossibleVenueIds = new HashSet<Int32>(users.SelectMany(u => u.VenuesRefusing).Select(v => v.VenueID));
                possibleVenues = possibleVenues.Where(pv => !impossibleVenueIds.Contains(pv.VenueID)).ToList();

                // Third, let's add one entry for every venue that is in neither the desired nor undesired lists 
/*              var possibleVenueIds = new HashSet<Int32>(possibleVenues.Select(pv => pv.VenueID));
                possibleVenues.AddRange(filteredVenues.Where(fv => !possibleVenueIds.Contains(fv.VenueID) && !impossibleVenueIds.Contains(fv.VenueID)));
*/
                usersToRemove++;

                result = new VenueFilterResult()
                {
                    Venues = possibleVenues,
                    HappyUsers = users,
                    UnhappyUsers = unhappyUsers
                };

            } while (!possibleVenues.Any() && usersToRemove < allUsers.Count());

            return result;
        }

        /// <summary>
        /// Gets the lunch of the day or creates it if it doesn't already exist
        /// </summary>
        /// <returns>Lunch</returns>
        private Lunch GetOrCreateLunch()
        {
            lock (lunchCreationLock)
            {
                var lunch = this.LunchRepository.FindLunchOfDay(DateTime.Now);

                if (lunch == null)
                {
                    var time = this.Settings.LunchStartTime;

                    lunch = new Lunch()
                    {
                        LunchTime = DateTime.Today + time
                    };

                    this.LunchRepository.Save(lunch);
                }

                return lunch;
            }
        }

        internal class VenueFilterResult
        {
            public IEnumerable<Venue> Venues { get; set; }

            public IEnumerable<User> HappyUsers { get; set; }

            public IEnumerable<User> UnhappyUsers { get; set; }

            public Int32 PercentHappy
            {
                get
                {
                    if (this.HappyUsers == null || this.UnhappyUsers == null)
                    {
                        return 0;
                    }

                    return (int)((double)this.HappyUsers.Count() / (double)(this.HappyUsers.Count() + UnhappyUsers.Count()) * 100);
                }
            }
        }
    }
}